/**
 * 上海中赢金融信息服务有限公司
 * Copyright (c) 2017-2027 Chinazyjr,Inc.All Rights Reserved.
 */

package com.sys.midware.mongo.core.mongoOP;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;

/**
 * mongo查询类
 * <b>ClassName:</b> MongoQuery.java <br/>
 * <b>Date:</b> 2016年1月11日 下午1:27:50<br/>
 * eg.
 *  DBObject query = new QueryBuilder().put(“sex”).is(“man”).and(“age”).greaterThan(20).get();
 *  int[] inList = {23,24};
    DBObject query = new QueryBuilder().put(“age”).notIn(inList).get();
    int[] inList = {23,24};
    DBObject query = new QueryBuilder().put(“age”).in(inList).get();
    DBObject query = new QueryBuilder().put(“age”).lessThanEquals(22).get();
 * 
 * @author mobing
 * @version
 */
public class MongoQuery {
    
    private DBObject _query;

    private String _currentKey;

    private boolean _hasNot;
    
    private static class NullObject {
    }
    /**
     * Creates a builder with an empty query
     */
    private MongoQuery() {
        _query = new BasicDBObject();
    }
    
    /**
     * returns a new QueryBuilder
     * @return
     */
    public static MongoQuery start() {
        return new MongoQuery();
    }
    
    /**
     * Creates a new query with a document key
     * @param key MongoDB document key
     * @return Returns a new QueryBuilder
     */
    public static MongoQuery start(String key) {
        return (new MongoQuery()).put(key);
    }
    
    /**
     * Adds a new key to the query if not present yet.
     * Sets this key as the current key.
     * @param key MongoDB document key
     * @return this
     */
    public MongoQuery put(String key) {
        _currentKey = key;
        if(_query.get(key) == null) {
            _query.put(_currentKey, new NullObject());
        }
        return this;
    }
    
    /**
     * Equivalent to <code>QueryBuilder.put(key)</code>. Intended for compound query chains to be more readable, e.g.
     * {@code QueryBuilder.start("a").greaterThan(1).and("b").lessThan(3) }
     * @param key MongoDB document key
     * @return this
     */
    public MongoQuery and(String key) {
        return put(key);
    }
    
    /**
     * Creates a <code>DBObject</code> query to be used for the driver's find operations
     * @return Returns a DBObject query instance
     * @throws RuntimeException if a key does not have a matching operand
     */
    public DBObject get() {
        for(String key : _query.keySet()) {
            if(_query.get(key) instanceof NullObject) {
                throw new MongoQueryException("No operand for key:" + key);
            }
        }
        return _query;
    }
    
    @SuppressWarnings("serial")
    private static class MongoQueryException extends RuntimeException {
            MongoQueryException(String message) {
                super(message);
            }
    }
    
    private void addOperand(String op, Object value) {
        if(op == null) {
            if (_hasNot) {
                value = new BasicDBObject(MongoOperators.NOT, value);
                _hasNot = false;
            }
            _query.put(_currentKey, value);
            return;
        }

        Object storedValue = _query.get(_currentKey);
        BasicDBObject operand;
        if(!(storedValue instanceof DBObject)) {
            operand = new BasicDBObject();
            if (_hasNot) {
                DBObject notOperand = new BasicDBObject(MongoOperators.NOT, operand);
                _query.put(_currentKey, notOperand);
                _hasNot = false;
            } else {
                _query.put(_currentKey, operand);
            }
        } else {
            operand = (BasicDBObject)_query.get(_currentKey);
            if (operand.get(MongoOperators.NOT) != null) {
                operand = (BasicDBObject) operand.get(MongoOperators.NOT);
            }
        }
        operand.put(op, value);
    }
    /**
     * Equivalent to $not meta operator. Must be followed by an operand, not a value, e.g.
     * {@code QueryBuilder.start("val").not().mod(Arrays.asList(10, 1)) }
     *
     * @return Returns the current QueryBuilder with an appended "not" meta operator
     */
    public MongoQuery not() {
        _hasNot = true;
        return this;
    }
    @SuppressWarnings("unchecked")
    public <T> MongoQuery and( DBObject ... ands ){
        List<Object> l = (List<Object>)_query.get( MongoOperators.AND );
        if ( l == null ){
            l = new ArrayList<Object>();
            _query.put( MongoOperators.AND , l );
        }
        Collections.addAll(l, ands);
        return this;
    }
    
    /**
     * Equivalent to an $or operand
     * @param ors the list of conditions to or together
     * @return Returns the current QueryBuilder with appended "or" operator
     */
    @SuppressWarnings("unchecked")
    public MongoQuery or( DBObject ... ors ){
        List<Object> l = (List<Object>)_query.get( MongoOperators.OR );
        if ( l == null ){
            l = new ArrayList<Object>();
            _query.put( MongoOperators.OR , l );
        }
        Collections.addAll(l, ors);
        return this;
    }
    
    /**
     * Equivalent of the $all operand
     * @param object Value to query
     * @return Returns the current QueryBuilder with an appended "matches all array contents" query
     */
    public MongoQuery all(Object object) {
        addOperand(MongoOperators.ALL, object);
        return this;
    }
    
    /**
     * Equivalent of the $size operand
     * @param object Value to query
     * @return Returns the current QueryBuilder with an appended size operator
     */
    public MongoQuery size(Object object) {
        addOperand(MongoOperators.SIZE, object);
        return this;
    }
    
    /**
     * Equivalent to the $gt operator
     * @param object Value to query
     * @return Returns the current QueryBuilder with an appended "greater than" query  
     */
    public MongoQuery greaterThan(Object object) {
        addOperand(MongoOperators.GT, object);
        return this;
    }
    
    /**
     * Equivalent to the $gte operator
     * @param object Value to query
     * @return Returns the current QueryBuilder with an appended "greater than or equals" query
     */
    public MongoQuery greaterThanEquals(Object object) {
        addOperand(MongoOperators.GTE, object);
        return this;
    }
    
    /**
     * Equivalent to the $lt operand
     * @param object Value to query
     * @return Returns the current QueryBuilder with an appended "less than" query
     */
    public MongoQuery lessThan(Object object) {
        addOperand(MongoOperators.LT, object);
        return this;
    }
    
    /**
     * Equivalent to the $lte operand
     * @param object Value to query
     * @return Returns the current QueryBuilder with an appended "less than or equals" query
     */
    public MongoQuery lessThanEquals(Object object) {
        addOperand(MongoOperators.LTE, object);
        return this;
    }
    
    /**
     * Equivalent of the find({key:value})
     * @param object Value to query
     * @return Returns the current QueryBuilder with an appended equality query
     */
    public MongoQuery is(Object object) {
        addOperand(null, object);
        return this;
    }
    
    /**
     * Equivalent of the $ne operand
     * @param object Value to query
     * @return Returns the current QueryBuilder with an appended inequality query
     */
    public MongoQuery notEquals(Object object) {
        addOperand(MongoOperators.NE, object);
        return this;
    }
    
    /**
     * Equivalent of the $in operand
     * @param object Value to query
     * @return Returns the current QueryBuilder with an appended "in array" query
     */
    public MongoQuery in(Object object) {
        addOperand(MongoOperators.IN, object);
        return this;
    }
    
    /**
     * Equivalent of the $nin operand
     * @param object Value to query
     * @return Returns the current QueryBuilder with an appended "not in array" query
     */
    public MongoQuery notIn(Object object) {
        addOperand(MongoOperators.NIN, object);
        return this;
    }
    
    /**
     * Equivalent of the $mod operand
     * @param object Value to query
     * @return Returns the current QueryBuilder with an appended modulo query
     */
    public MongoQuery mod(Object object) {
        addOperand(MongoOperators.MOD, object);
        return this;
    }
    
    
    /**
     * Equivalent of the $exists operand
     * @param object Value to query
     * @return Returns the current QueryBuilder with an appended exists operator
     */
    public MongoQuery exists(Object object) {
        addOperand(MongoOperators.EXISTS, object);
        return this;
    }
    
    /**
     * Passes a regular expression for a query
     * @param regex Regex pattern object
     * @return Returns the current QueryBuilder with an appended regex query
     */
    public MongoQuery regex(Pattern regex) {
        addOperand(null, regex);
        return this;
    }

    /**
     * Equivalent to the $elemMatch operand
     * @param match  the object to match
     * @return Returns the current QueryBuilder with an appended elemMatch operator
     */
    public MongoQuery elemMatch(final DBObject match) {
        addOperand(MongoOperators.ELEM_MATCH, match);
        return this;
    }
    
    /**
     * Equivalent of the $within operand, used for geospatial operation
     * @param x x coordinate
     * @param y y coordinate
     * @param radius radius
     * @return
     */
    public MongoQuery withinCenter( double x , double y , double radius ){
        addOperand( MongoOperators.WITHIN ,
                    new BasicDBObject(MongoOperators.CENTER, new Object[]{ new Double[]{ x , y } , radius } ) );
        return this;
    }
    
    /**
     * Equivalent of the $near operand
     * @param x x coordinate
     * @param y y coordinate
     * @return
     */
    public MongoQuery near( double x , double y  ){
        addOperand(MongoOperators.NEAR,
                    new Double[]{ x , y } );
        return this;
    }

    /**
     * Equivalent of the $near operand
     * @param x x coordinate
     * @param y y coordinate
     * @param maxDistance max distance
     * @return
     */
    public MongoQuery near( double x , double y , double maxDistance ){
        addOperand( MongoOperators.NEAR ,
                    new Double[]{ x , y , maxDistance } );
        return this;
    }
    
    /**
     * Equivalent of the $nearSphere operand
     * @param longitude coordinate in decimal degrees 
     * @param latitude coordinate in decimal degrees
     * @return
     */
    public MongoQuery nearSphere( double longitude , double latitude ){
        addOperand(MongoOperators.NEAR_SPHERE,
                    new Double[]{ longitude , latitude } );
        return this;
    }
    
    /**
     * Equivalent of the $nearSphere operand
     * @param longitude coordinate in decimal degrees 
     * @param latitude coordinate in decimal degrees
     * @param maxDistance max spherical distance
     * @return
     */
    public MongoQuery nearSphere( double longitude , double latitude , double maxDistance ){
        addOperand( MongoOperators.NEAR_SPHERE ,
                    new Double[]{ longitude , latitude , maxDistance } );
        return this;
    }
    
    /**
     * Equivalent of the $centerSphere operand
     * mostly intended for queries up to a few hundred miles or km.
     * @param longitude coordinate in decimal degrees 
     * @param latitude coordinate in decimal degrees
     * @param maxDistance max spherical distance
     * @return
     */
    public MongoQuery withinCenterSphere( double longitude , double latitude , double maxDistance ){
        addOperand( MongoOperators.WITHIN ,
                new BasicDBObject(MongoOperators.CENTER_SPHERE, new Object[]{ new Double[]{longitude , latitude} , maxDistance } ) );
        return this;
    }
    
    /**
     * Equivalent to a $within operand, based on a bounding box using represented by two corners
     * 
     * @param x the x coordinate of the first box corner.
     * @param y the y coordinate of the first box corner.
     * @param x2 the x coordinate of the second box corner.
     * @param y2 the y coordinate of the second box corner.
     * @return
     */
    public MongoQuery withinBox(double x, double y, double x2, double y2) {
        addOperand( MongoOperators.WITHIN ,
                    new BasicDBObject(MongoOperators.BOX, new Object[] { new Double[] { x, y }, new Double[] { x2, y2 } } ) );
        return this;
    }
    
    /**
     * Equivalent to a $within operand, based on a bounding polygon represented by an array of points
     * 
     * @param points an array of Double[] defining the vertices of the search area
     * @return this
     */
    public MongoQuery withinPolygon(List<Double[]> points) {
        if(points == null || points.isEmpty() || points.size() < 3)
            throw new IllegalArgumentException("Polygon insufficient number of vertices defined");
        addOperand( MongoOperators.WITHIN ,
                    new BasicDBObject(MongoOperators.POLYGON, points ) );
        return this;
    }



}
